<?php

/**
 * Shopalong shopping cart class
 *
 * @todo We're currently assigning the cart object to the session cart at the
 * end of each  method which alters any part of the cart.  It would be nice if
 * this were being done automatically but in the meantime, please be careful
 * with code changes which affect the cart object to ensure they are passed to
 * the session cart.
 *
 * @todo We need to handle the options (previously attributes) better
 *
 * Copyright (c) 2003 osCommerce
 * Portions Copyright (c) 2006 Shopalong
 *
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
 */

class ShoppingCart
{
	var $MIXED_TYPE = 'mixed';
	var $PHYSICAL_TYPE = 'physical';
	var $VIRTUAL_TYPE = 'virtual';
	
	var $_contents = array();
	var $_contentType;
	var $_id;
	var $_total;
	var $_weight;
	
	// SQL statements
	var $_addProductSql = 'insert into %s (customers_id, products_id, customers_basket_quantity, customers_basket_date_added) values ("%s", "%s", "%s", "%s")';
	var $_addAttributeSql = 'insert into %s (customers_id, products_id, products_options_id, products_options_value_id) values ("%s", "%s", "%s", "%s")';
	var $_attributeSql = 'select options_values_price, price_prefix from %s where products_id = "%s" and options_id = "%s" and options_values_id = "%s"';
	var $_virtualCheckSql = 'select count(*) as total from %s pa, %s pad where pa.products_id = "%s" and pa.options_values_id = "%s" and pa.products_attributes_id = pad.products_attributes_id';
	var $_deleteProductSql = 'delete from %s where customers_id = "%s" and products_id = "%s"';
	var $_deleteAttributesSql = 'delete from %s where customers_id = "%s" and products_id = "%s"';
	var $_updateCartQuantitySql = 'update %s set customers_basket_quantity = "%s" where customers_id = "%s" and products_id = "%s"';
	var $_updateAttributeQuantitySql = 'update %s set products_options_value_id = "%s" where customers_id = "%s" and products_id = "%s" and products_options_id = "%s"';
	var $_calculateProductSql = 'select products_id, products_price, products_tax_class_id, products_weight from %s where products_id = "%s"';
	var $_calculateSpecialsSql = 'select specials_new_products_price from %s where products_id = "%s" and status = "1"';
	var $_calculateAttributePriceSql = 'select options_values_price, price_prefix from %s where products_id = "%s" and options_id = "%s" and options_values_id = "%s"';
	var $_resetCartSql = 'delete from %s where customers_id = "%s"';
	var $_resetCartAttributeSql = 'delete from %s where customers_id = "%s"';
	var $_restoreProductsSql = 'select products_id from %s where customers_id = "%s" and products_id = "%s"';
	var $_restoreProductQuantitySql = 'select products_id, customers_basket_quantity from %s where customers_id = "%s"';
	var $_restoreAttributesSql = 'select products_options_id, products_options_value_id from %s where customers_id = "%s" and products_id = "%s"';
	
	/**
	 * Constructor function
	 *
	 * @access public
	 */
	
	function ShoppingCart()
	{
		global $sessioncart, $session;
		
		// Restore the user's shopping cart
		if ($session->isRegistered('sessioncart') && is_object($sessioncart))
		{
			if (PHP_VERSION < 4)
			{
				$brokenCart = $sessioncart;
				$this->_unserialize($brokenCart);
				$sessioncart = $this;
			}
			
			else
			{
				// PHP5 doesn't seem to allow re-assignment of $this, so we need
				// to map object variables of $sessioncart to $this
				// $this = $sessioncart;
				$mapVars = get_class_vars('ShoppingCart');
				
				foreach($mapVars AS $key=>$value) {
					$this->$key = &$sessioncart->$key;
				}
			}
		}
		
		// or create a new one if it doesn't exist
		else
		{
			$session->register('sessioncart');
			$this->reset();
			$sessioncart = $this;
		}
	}
	
	/**
	 * Add a product to the cart
	 *
	 * @todo Move product notifications to a plugin
	 *
	 * @access public
	 * @param integer $productsId
	 * @param integer $quantity Default: 1
	 * @param array attributes Default: empty
	 * @param boolean notify ? (default true)
	 */
	
	function addProduct($productsId, $quantity=1, $attributes='', $notify=true)
	{
		global $new_products_id_in_cart, $customer_id, $sessioncart, $session;
		
		$productsId = tep_get_uprid($productsId, $attributes);
		
		if ($notify == true)
		{
			$new_products_id_in_cart = $productsId;
			$session->register('new_products_id_in_cart');
		}
		
		if ($this->containsProduct($productsId))
		{
			$this->setProductQuantity($productsId, $quantity, $attributes);
		}
		
		else
		{
			$this->_contents[$productsId] = array('qty' => $quantity);
			
			if ($session->isRegistered('customer_id'))
			{
				tep_db_query(sprintf($this->_addProductSql, TABLE_CUSTOMERS_BASKET, (int)$customer_id, tep_db_input($productsId), $quantity, date('Ymd')));
				
				if (is_array($attributes))
				{
					reset($attributes);
					
					while (list($option, $value) = each($attributes))
					{
						$this->_contents[$productsId]['attributes'][$option] = $value;
						
						if ($session->isRegistered('customer_id'))
						{
							tep_db_query(sprintf($this->_addAttributeSql, TABLE_CUSTOMERS_BASKET_ATTRIBUTES, (int)$customer_id, tep_db_input($productsId), (int)$option, (int)$value));
						}
					}
				}
			}
		}
		
		$this->_cleanup();
		
		// assign a temporary unique ID to the order contents to prevent hack
		// attempts during the checkout procedure
		$this->_id = $this->_generateId();
		$sessioncart = $this;
	}
	
	/**
	 * Check if the cart contains a particular product
	 *
	 * @access public
	 * @param integer $productsId
	 * @return boolean
	 */
	
	function containsProduct($productsId)
	{
		if (isset($this->_contents[$productsId]))
		{
			return true;
		}
		
		return false;
	}
	
	/**
	 * Get price for a products attribute
	 *
	 * @access public
	 * @param integer $productsId
	 * @return float
	 */
	
    function getAttributesPrice($productsId)
	{
		$attributes_price = 0;
		
		if (isset($this->_contents[$productsId]['attributes']))
		{
			reset($this->_contents[$productsId]['attributes']);
			
			while (list($option, $value) = each($this->_contents[$productsId]['attributes']))
			{
				$attribute_price_query = tep_db_query(sprintf($this->_attributeSql, TABLE_PRODUCTS_ATTRIBUTES, (int)$productsId, (int)$option, (int)$value));
				$attribute_price = tep_db_fetch_array($attribute_price_query);
				
				if ($attribute_price['price_prefix'] == '+')
				{
					$attributes_price += $attribute_price['options_values_price'];
				}
				
				else
				{
					$attributes_price -= $attribute_price['options_values_price'];
				}
			}
		}
		
		return $attributes_price;
	}
	
	/**
	 * Get the content type of the cart
	 *
	 * @access public
	 * @return string One of $MIXED_TYPE, $PHYSICAL_TYPE or $VIRTUAL_TYPE
	 */
	
	function getContentType()
	{
		global $sessioncart;
		
		$this->_contentType = false;
		
		if ( (DOWNLOAD_ENABLED == 'true') && ($this->getTotalProducts() > 0) )
		{
			reset($this->_contents);
			
			while (list($productsId, ) = each($this->_contents))
			{
				if (isset($this->_contents[$productsId]['attributes']))
				{
					reset($this->_contents[$productsId]['attributes']);
					
					while (list(, $value) = each($this->_contents[$productsId]['attributes']))
					{
						$virtual_check_query = tep_db_query(sprintf($this->_virtualCheckSql, TABLE_PRODUCTS_ATTRIBUTES, TABLE_PRODUCTS_ATTRIBUTES_DOWNLOAD, (int)$productsId, (int)$value));
						$virtual_check = tep_db_fetch_array($virtual_check_query);
						
						if ($virtual_check['total'] > 0)
						{
							switch ($this->_contentType)
							{
								case $this->PHYSICAL_TYPE:
								$this->_contentType = $this->MIXED_TYPE;
								return $this->_contentType;
								break;
								
								default:
								$this->_contentType = $this->VIRTUAL_TYPE;
								break;
							}
						}
						
						else
						{
							switch ($this->_contentType)
							{
								case $this->VIRTUAL_TYPE:
								$this->_contentType = $this->MIXED_TYPE;
								return $this->_contentType;
								break;
								
								default:
								$this->_contentType = $this->PHYSICAL_TYPE;
								break;
							}
						}
					}
				}
				
				else
				{
					switch ($this->_contentType)
					{
						case $this->VIRTUAL_TYPE:
						$this->_contentType = $this->MIXED_TYPE;
						return $this->_contentType;
						break;
						
						default:
						$this->_contentType = $this->PHYSICAL_TYPE;
						break;
					}
				}
			}
		}
		
		else
		{
			$this->_contentType = $this->PHYSICAL_TYPE;
		}
		
		$sessioncart = $this;
		return $this->_contentType;
	}
	
	/**
	 * Get total number of products in cart
	 *
	 * @access public
	 * @return integer
	 */
	
	function getProductCount()
	{
		$total_items = 0;
		
		if (is_array($this->_contents))
		{
			reset($this->_contents);
			
			while (list($productsId, ) = each($this->_contents))
			{
				$total_items += $this->getProductQuantity($productsId);
			}
		}
		
		return $total_items;
	}
	
	/**
	 * Get a list of the product ids in the cart
	 *
	 * @access public
	 * @return array
	 */
	
	function getProductIdList()
	{
		$productIdList = array();
		
		if (is_array($this->_contents))
		{
			reset($this->_contents);
			
			while (list($productsId, ) = each($this->_contents))
			{
				$productIdList[] = $productsId;
			}
		}
		
		return $productIdList;
	}
	
	/**
	 * Get the quantity of a specified product in the cart
	 *
	 * @access public
	 * @param integer $productsId
	 * @return integer
	 */
	
	function getProductQuantity($productsId)
	{
		if (isset($this->_contents[$productsId]))
		{
			return $this->_contents[$productsId]['qty'];
		}
		
		return 0;
	}
	
	/**
	 * Get an array of products in the cart
	 *
	 * @access public
	 * @return array Array of Product objects
	 */
	
	function getProducts()
	{
		global $lng;
		
		$products_array = array();
		reset($this->_contents);
		
		while (list($productId, ) = each($this->_contents))
		{
			$product = new Product($productId);
			
			if ($product->exists())
			{
				$products_array[] = $product;
			}
		}
		
		return $products_array;
	}
	
	/**
	 * Get the total value of items in the cart
	 *
	 * @access public
	 * @return float
	 */
	
	function getTotalAmount()
	{
		$this->_calculate();
		return $this->_total;
	}
	
	/**
	 * Get the total value of items in the cart
	 *
	 * @access public
	 * @return float
	 */
	
	function getTotalAmountFormatted()
	{
		global $currencies;
		
		$this->_calculate();
		return $currencies->displayPrice($this->_total);
	}
	
	/**
	 * Get the total weight of items in the cart
	 *
	 * @todo Confirm return type and specify units
	 *
	 * @access public
	 * @return float
	 */
	
	function getTotalWeight()
	{
		$this->_calculate();
		return $this->_weight;
	}
	
	/**
	 * Remove a product from the cart
	 *
	 * @access public
	 * @param integer $productsId
	 */
	
	function removeProduct($productsId)
	{
		global $customer_id, $sessioncart, $session;
		
		unset($this->_contents[$productsId]);
		
		// remove from database
		if ($session->isRegistered('customer_id'))
		{
			tep_db_query(sprintf($this->_deleteProductSql, TABLE_CUSTOMERS_BASKET, (int)$customer_id, tep_db_input($productsId)));
			
			tep_db_query(sprintf($this->_deleteAttributesSql, TABLE_CUSTOMERS_BASKET_ATTRIBUTES, (int)$customer_id, tep_db_input($productsId)));
		}
		
		// assign a temporary unique ID to the order contents to prevent hack attempts during the checkout procedure
		$this->_id = $this->_generateId();
		$sessioncart = $this;
	}
	
	/**
	 * Remove all products from the cart
	 *
	 * @access public
	 */
	
	function removeAllProducts()
	{
		$this->reset();
	}
	
	/**
	 * Reset the cart session (and optionally database contents)
	 *
	 * @access public
	 * @param boolean $resetDatabase Should the database contents be reset
	 */
	
	function reset($resetDatabase = false)
	{
		global $customer_id, $sessioncart, $session;
		
		$this->_contents = array();
		$this->_total = 0;
		$this->_weight = 0;
		$this->_contentType = false;
		
		if ($session->isRegistered('customer_id') && ($resetDatabase == true))
		{
			tep_db_query(sprintf($this->resetCartSql, TABLE_CUSTOMERS_BASKET, (int)$customer_id));
			tep_db_query(sprintf($this->resetCartAttributeSql, TABLE_CUSTOMERS_BASKET_ATTRIBUTES, (int)$customer_id));
		}
		
		unset($this->_id);
		
		if ($session->isRegistered('cartID'))
		{
			$session->unregister('cartID');
		}
		
		$sessioncart = $this;
	}
	
	/**
	 * Restore the contents of the cart
	 *
	 * @access public
	 */
	
	function restoreContents()
	{
		global $customer_id, $sessioncart, $session;
		
		if (!$session->isRegistered('customer_id'))
		{
			return;
		}
		
		// insert current cart contents in database
		if (is_array($this->_contents))
		{
			reset($this->_contents);
			
			while (list($productsId, ) = each($this->_contents))
			{
				$qty = $this->_contents[$productsId]['qty'];
				$product_query = tep_db_query(sprintf($this->_restoreProductsSql, TABLE_CUSTOMERS_BASKET, (int)$customer_id, tep_db_input($productsId)));
				
				if (!tep_db_num_rows($product_query))
				{
					tep_db_query(sprintf($this->_addProductSql, TABLE_CUSTOMERS_BASKET, (int)$customer_id, tep_db_input($productsId), $qty, date('Ymd')));
					
					if (isset($this->_contents[$productsId]['attributes']))
					{
						reset($this->_contents[$productsId]['attributes']);
						
						while (list($option, $value) = each($this->_contents[$productsId]['attributes']))
						{
							tep_db_query(sprintf($this->_addAttributeSql, TABLE_CUSTOMERS_BASKET_ATTRIBUTES, (int)$customer_id, tep_db_input($productsId), (int)$option, (int)$value));
						}
					}
				} else {
					tep_db_query(sprintf($this->_updateCartQuantitySql, TABLE_CUSTOMERS_BASKET, $qty, (int)$customer_id, tep_db_input($productsId)));
				}
			}
		}
		
		// reset per-session cart contents, but not the database contents
		$this->reset(false);
		$products_query = tep_db_query(sprintf($this->_restoreProductQuantitySql, TABLE_CUSTOMERS_BASKET, (int)$customer_id));
		
		while ($products = tep_db_fetch_array($products_query))
		{
			$this->_contents[$products['products_id']] = array('qty' => $products['customers_basket_quantity']);
			// attributes
			$attributes_query = tep_db_query(sprintf($this->_restoreAttributesSql, TABLE_CUSTOMERS_BASKET_ATTRIBUTES, (int)$customer_id, tep_db_input($products['products_id'])));
			
			while ($attributes = tep_db_fetch_array($attributes_query))
			{
				$this->_contents[$products['products_id']]['attributes'][$attributes['products_options_id']] = $attributes['products_options_value_id'];
			}
		}
		
		$this->_cleanup();
		$sessioncart = $this;
	}
	
	/**
	 * Set the quantity of a product in the cart
	 *
	 * @access public
	 * @param integer $productsId
	 * @param integer $quantity
	 * @param array $attributes Default: empty
	 */
	
	function setProductQuantity($productsId, $quantity, $attributes='')
	{
		global $customer_id, $sessioncart, $session;
		
		if ($quantity == '0')
		{
			$this->removeProduct($productsId);
			return;
		}
		
		// nothing needs to be updated if theres no quantity, so we return true
		if ($quantity < 0)
		{
			return;
		}
		
		$this->_contents[$productsId] = array('qty' => $quantity);
		
		// update database
		if ($session->isRegistered('customer_id'))
		{
			tep_db_query(sprintf($this->_updateCartQuantitySql, TABLE_CUSTOMERS_BASKET, $quantity, (int)$customer_id, tep_db_input($productsId)));
		}
		
		if (is_array($attributes))
		{
			reset($attributes);
			
			while (list($option, $value) = each($attributes))
			{
				$this->_contents[$productsId]['attributes'][$option] = $value;
				
				// update database
				if ($session->isRegistered('customer_id'))
				{
					tep_db_query(sprintf($this->_updateAttributeQuantitySql, TABLE_CUSTOMERS_BASKET_ATTRIBUTES, (int)$value, (int)$customer_id, tep_db_input($productsId), (int)$option));
				}
			}
		}
		
		$sessioncart = $this;
	}
	
	/**
	 * Calculate the cart totals (amount, weight)
	 *
	 * @access private
	 */
	
	function _calculate()
	{
		global $sessioncart;
		
		$this->_total = 0;
		$this->_weight = 0;
		reset($this->_contents);
		
		while (list($productsId, ) = each($this->_contents))
		{
			$qty = $this->_contents[$productsId]['qty'];
			
			// products price
			$product_query = tep_db_query(sprintf($this->_calculateProductSql, TABLE_PRODUCTS, (int)$productsId));
			
			if ($product = tep_db_fetch_array($product_query))
			{
				$prid = $product['products_id'];
				$products_tax = tep_get_tax_rate($product['products_tax_class_id']);
				$products_price = $product['products_price'];
				$products_weight = $product['products_weight'];
				$specials_query = tep_db_query(sprintf($this->_calculateSpecialsSql, TABLE_SPECIALS, (int)$prid));
				
				if (tep_db_num_rows ($specials_query))
				{
					$specials = tep_db_fetch_array($specials_query);
					$products_price = $specials['specials_new_products_price'];
				}
				
				$this->_total += tep_add_tax($products_price, $products_tax) * $qty;
				$this->_weight += ($qty * $products_weight);
			}
			
			// attributes price
			if (isset($this->_contents[$productsId]['attributes']))
			{
				reset($this->_contents[$productsId]['attributes']);
				
				while (list($option, $value) = each($this->_contents[$productsId]['attributes']))
				{
					$attribute_price_query = tep_db_query(sprintf($this->_calculateAttributePriceSql, TABLE_PRODUCTS_ATTRIBUTES, (int)$prid, (int)$option, (int)$value));
					$attribute_price = tep_db_fetch_array($attribute_price_query);
					
					if ($attribute_price['price_prefix'] == '+')
					{
						$this->_total += $qty * tep_add_tax($attribute_price['options_values_price'], $products_tax);
					}
					
					else
					{
						$this->_total -= $qty * tep_add_tax($attribute_price['options_values_price'], $products_tax);
					}
				}
			}
		}
		
		$sessioncart = $this;
	}
	
	/**
	 * Cleanup the cart
	 *
	 * @access private
	 */
	
	function _cleanup()
	{
		global $customer_id, $sessioncart, $session;
		
		reset($this->_contents);
		
		while (list($key,) = each($this->_contents))
		{
			if ($this->_contents[$key]['qty'] < 1)
			{
				unset($this->_contents[$key]);
				
				// remove from database
				if ($session->isRegistered('customer_id'))
				{
					tep_db_query(sprintf($this->_deleteProductSql, TABLE_CUSTOMERS_BASKET, (int)$customer_id, tep_db_input($key)));
					tep_db_query(sprintf($this->_deleteAttributesSql, TABLE_CUSTOMERS_BASKET_ATTRIBUTES, (int)$customer_id, tep_db_input($key)));
				}
			}
		}
		
		$sessioncart = $this;
	}
	
	/**
	 * Generate a unique id for the cart
	 *
	 * @access private
	 * @param integer $idStringLength Default: 5
	 */
	
	function _generateId($idStringLength=5)
	{
		return tep_create_random_value($idStringLength, 'digits');
	}
	
	/**
	 * Unserialize the cart
	 *
	 * @access private
	 * @param array $broken The serialized cart to be restored
	 */
	
	function _unserialize($broken)
	{
		global $sessioncart;
		
		for(reset($broken); $kv=each($broken);)
		{
			$key=$kv['key'];
			
			if (gettype($this->$key)!='user function')
			{
				$this->$key=$kv['value'];
			}
		}
		
		$sessioncart = $this;
	}
}

?>
